// Copyright Epic Games, Inc. All Rights Reserved.

#include "zenutil/logging.h"

ZEN_THIRD_PARTY_INCLUDES_START
#include <spdlog/async.h>
#include <spdlog/async_logger.h>
#include <spdlog/sinks/ansicolor_sink.h>
#include <spdlog/sinks/msvc_sink.h>
#include <spdlog/spdlog.h>
ZEN_THIRD_PARTY_INCLUDES_END

#include <zencore/compactbinary.h>
#include <zencore/filesystem.h>
#include <zencore/logging.h>
#include <zencore/string.h>
#include <zenutil/logging/fullformatter.h>
#include <zenutil/logging/jsonformatter.h>
#include <zenutil/logging/rotatingfilesink.h>

#include <chrono>
#include <memory>

namespace zen {

static bool		 g_IsLoggingInitialized;
spdlog::sink_ptr g_FileSink;

spdlog::sink_ptr
GetFileSink()
{
	return g_FileSink;
}

void
InitializeLogging(const LoggingOptions& LogOptions)
{
	BeginInitializeLogging(LogOptions);
	FinishInitializeLogging(LogOptions);
}

void
BeginInitializeLogging(const LoggingOptions& LogOptions)
{
	zen::logging::InitializeLogging();
	zen::logging::EnableVTMode();

	bool IsAsync = true;

	if (LogOptions.IsDebug)
	{
		IsAsync = false;
	}

	if (LogOptions.IsTest)
	{
		IsAsync = false;
	}

	if (IsAsync)
	{
		const int QueueSize	  = 8192;
		const int ThreadCount = 1;
		spdlog::init_thread_pool(QueueSize, ThreadCount, [&] { SetCurrentThreadName("spdlog_async"); });

		auto AsyncSink = spdlog::create_async<spdlog::sinks::ansicolor_stdout_sink_mt>("main");
		zen::logging::SetDefault("main");
	}

	// Sinks

	spdlog::sink_ptr FileSink;

	// spdlog can't create directories that starts with `\\?\` so we make sure the folder exists before creating the logger instance

	if (!LogOptions.AbsLogFile.empty())
	{
		if (LogOptions.AbsLogFile.has_parent_path())
		{
			zen::CreateDirectories(LogOptions.AbsLogFile.parent_path());
		}

		FileSink = std::make_shared<zen::logging::RotatingFileSink>(LogOptions.AbsLogFile,
																	/* max size */ 128 * 1024 * 1024,
																	/* max files */ 16,
																	/* rotate on open */ true);
		if (LogOptions.AbsLogFile.extension() == ".json")
		{
			FileSink->set_formatter(std::make_unique<logging::json_formatter>(LogOptions.LogId));
		}
		else
		{
			FileSink->set_formatter(std::make_unique<logging::full_formatter>(LogOptions.LogId));  // this will have a date prefix
		}
	}

	std::set_terminate([]() { ZEN_CRITICAL("Program exited abnormally via std::terminate()"); });

	// Default

	LoggerRef DefaultLogger = zen::logging::Default();
	auto&	  Sinks			= DefaultLogger.SpdLogger->sinks();

	Sinks.clear();

	if (LogOptions.NoConsoleOutput)
	{
		zen::logging::SuppressConsoleLog();
	}
	else
	{
		auto ConsoleSink = std::make_shared<spdlog::sinks::ansicolor_stdout_sink_mt>();
		Sinks.push_back(ConsoleSink);
	}

	if (FileSink)
	{
		Sinks.push_back(FileSink);
	}

#if ZEN_PLATFORM_WINDOWS
	if (zen::IsDebuggerPresent() && LogOptions.IsDebug)
	{
		auto DebugSink = std::make_shared<spdlog::sinks::msvc_sink_mt>();
		DebugSink->set_level(spdlog::level::debug);
		Sinks.push_back(DebugSink);
	}
#endif

	spdlog::set_error_handler([](const std::string& msg) {
		if (msg == std::bad_alloc().what())
		{
			// Don't report out of memory in spdlog as we usually log in response to errors which will cause another OOM crashing the
			// program
			return;
		}
		// Bypass zen logging wrapping to reduce potential other error sources
		if (auto ErrLogger = zen::logging::ErrorLog())
		{
			try
			{
				ErrLogger.SpdLogger->log(spdlog::level::err, msg);
			}
			catch (const std::exception&)
			{
				// Just ignore any errors when in error handler
			}
		}
		try
		{
			Log().SpdLogger->error(msg);
		}
		catch (const std::exception&)
		{
			// Just ignore any errors when in error handler
		}
	});

	g_FileSink = std::move(FileSink);
}

void
FinishInitializeLogging(const LoggingOptions& LogOptions)
{
	logging::level::LogLevel LogLevel = logging::level::Info;

	if (LogOptions.IsDebug)
	{
		LogLevel = logging::level::Debug;
	}

	if (LogOptions.IsTest)
	{
		LogLevel = logging::level::Trace;
	}

	// Configure all registered loggers according to settings

	logging::RefreshLogLevels(LogLevel);
	spdlog::flush_on(spdlog::level::err);
	spdlog::flush_every(std::chrono::seconds{2});
	spdlog::set_formatter(
		std::make_unique<logging::full_formatter>(LogOptions.LogId, std::chrono::system_clock::now()));	 // default to duration prefix

	if (LogOptions.AbsLogFile.extension() == ".json")
	{
		g_FileSink->set_formatter(std::make_unique<logging::json_formatter>(LogOptions.LogId));
	}
	else
	{
		g_FileSink->set_formatter(std::make_unique<logging::full_formatter>(LogOptions.LogId));	 // this will have a date prefix
	}

	const std::string StartLogTime = zen::DateTime::Now().ToIso8601();

	spdlog::apply_all([&](auto Logger) { Logger->info("log starting at {}", StartLogTime); });

	g_IsLoggingInitialized = true;
}

void
ShutdownLogging()
{
	if (g_IsLoggingInitialized)
	{
		auto DefaultLogger = zen::logging::Default();
		ZEN_LOG_INFO(DefaultLogger, "log ending at {}", zen::DateTime::Now().ToIso8601());
	}

	zen::logging::ShutdownLogging();

	g_FileSink.reset();
}

}  // namespace zen
